﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Jay.Sc2.Bof.Lib.Tools;

namespace Jay.Sc2.Bof.Lib.Entity
{

	public class ProductionInformation
	{
		public int Time;
		public IEntity Producer;
		public IDictionary<IEntity, int> Input;
	}
	public class BuildInformation
	{
		public IEntity Entity;
		public ProductionInformation Information = new ProductionInformation();

		public override string ToString()
		{
			return "[" + Entity + " by " + Information.Producer + " : " + Information.Time + " -> " + (Entity.Time + Information.Time) + "]";
		}
	}
	public class EntityState : Dictionary<IEntity, int>, IEquatable<EntityState>, ICloneable
	{
		public int Time;
		public int Supply;
		public int Food;
		public double Minerals;
		public double Gas;

		public EntityState Goal;
		public double EstimatedTimeLeft;

		public int HarvesterOnOther;
		public int HarvesterOnBuilding;
		public int HarvesterOnGas;
		public int HarvesterOnMinerals;

		public List<BuildInformation> BuildTimes;
		public Dictionary<IEntity, int> Production = new Dictionary<IEntity, int>();
		public Dictionary<IEntity, int> Producing = new Dictionary<IEntity, int>();

		public int MineralsSpent;
		public int GasSpent;

		public int NextBuildTimeIndex = 0;
		
		private double m_MineralIncomePerGameSecond;
		private double m_GasIncomePerGameSecond;

		public EntityState()
		{
			BuildTimes = new List<BuildInformation>();
			Production = new Dictionary<IEntity, int>();
		}
		public EntityState(EntityState state)
			: base(state)
		{
			Time = state.Time;
			Supply = state.Supply;
			Food = state.Food;
			Minerals = state.Minerals;
			Gas = state.Gas;

			HarvesterOnOther = state.HarvesterOnOther;
			HarvesterOnBuilding = state.HarvesterOnBuilding;
			HarvesterOnGas = state.HarvesterOnGas;
			HarvesterOnMinerals = state.HarvesterOnMinerals;

			BuildTimes = new List<BuildInformation>(state.BuildTimes);
			Production = new Dictionary<IEntity, int>(state.Production);
			Producing = new Dictionary<IEntity, int>(state.Producing);

			Goal = state.Goal;
			EstimatedTimeLeft = state.EstimatedTimeLeft;

			MineralsSpent = state.MineralsSpent;
			GasSpent = state.GasSpent;
			
			m_MineralIncomePerGameSecond = state.MineralIncomePerGameSecond;
			m_GasIncomePerGameSecond = state.MineralIncomePerGameSecond;

			NextBuildTimeIndex = state.NextBuildTimeIndex;
		}

		public override string ToString()
		{
			return new TimeSpan(0, 0, Time) + " + " + new TimeSpan(0, 0, (int)EstimatedTimeLeft) + " => " + new TimeSpan(0, 0, (int)EstimatedTotalTime);
		}
		public string Text { get { return ToString(); } }

		public int ChangeProduction(IEntity entity, int delta)
		{
			int count = 0;
			Production.TryGetValue(entity, out count);
			count += delta;
			if (count == 0) Production.Remove(entity);
			else Production[entity] = count;
			return count;
		}
		public int ChangeProducing(IEntity entity, int delta)
		{
			int count = 0;
			Producing.TryGetValue(entity, out count);
			count += delta;
			if (count == 0) Producing.Remove(entity);
			else Producing[entity] = count;
			return count;
		}
		public int ChangeEntity(IEntity entity, int delta)
		{
			int count = 0;
			TryGetValue(entity, out count);
			count += delta;
			if (count == 0) Remove(entity);
			else this[entity] = count;
			return count;
		}
		public int MaxBuildTime(IEntity entity)
		{
			int timeLeft = 0;
			for (int c = NextBuildTimeIndex; c < BuildTimes.Count; c++)
			{
				BuildInformation info = BuildTimes[c];
				if (info.Entity == entity)
					timeLeft = Math.Max(timeLeft, info.Entity.Time - (Time - info.Information.Time));
			}
			return timeLeft;
		}
		public int MinBuildTime(IEntity entity)
		{
			if (NextBuildTimeIndex >= BuildTimes.Count) return 0;
			int timeLeft = int.MaxValue;
			for (int c = NextBuildTimeIndex; c < BuildTimes.Count; c++)
			{
				BuildInformation info = BuildTimes[c];
				if (info.Entity == entity)
					timeLeft = Math.Min(timeLeft, info.Entity.Time - (Time - info.Information.Time));
			}
			return timeLeft;
		}
		public int CountEntity(IEntity entity)
		{
			int count = 0;
			if (!TryGetValue(entity, out count)) return 0;
			return count;
		}
		public int CountProduction(IEntity entity)
		{
			int count = 0;
			if (!Production.TryGetValue(entity, out count)) return 0;
			return count;
		}
		public int CountProducing(IEntity entity)
		{
			int count = 0;
			if (!Producing.TryGetValue(entity, out count)) return 0;
			return count;
		}
		public bool HaveRequirement(IEntity entity) { return CountEntity(entity) > 0; }
		public bool HaveProducerAvailable(IEntity entity) { return CountEntity(entity) - CountProducing(entity) > 0; }
		public bool CanBuild(IEntity entity, out IEntity producerToUse, out IDictionary<IEntity, int> inputToUse)
		{
			producerToUse = null;
			inputToUse = null;
			if (entity.Gas > Gas) return false;
			if (entity.Minerals > Minerals) return false;
			if (entity.Name == "Refinery")
			{
				if (CountEntity(entity) + CountProduction(entity) >= 2) return false;
			}
			IUnit unit = entity as IUnit;
			if (unit != null && Food + unit.Food > Supply) return false;
			foreach (IEntity requirement in entity.Requirements)
				if (!HaveRequirement(requirement)) return false;
			if (entity.Producers.Count > 0)
			{
				bool canProduce = false;
				foreach (IEntity producer in entity.Producers)
					if (HaveProducerAvailable(producer))
					{
						canProduce = true;
						producerToUse = producer;
						break;
					}
				if (!canProduce) return false;
			}
			if (entity.Inputs.Count > 0)
			{
				bool haveAtLeastOneInput = false;
				foreach (IDictionary<IEntity, int> set in entity.Inputs)
				{
					bool haveInput = true;
					foreach (KeyValuePair<IEntity, int> input in set)
					{
						int count = CountEntity(input.Key);
						if (count < input.Value)
						{
							haveInput = false;
							break;
						}
					}
					if (haveInput)
					{
						haveAtLeastOneInput = true;
						inputToUse = set;
						break;
					}
				}
				if (!haveAtLeastOneInput) return false;
			}
			if (entity is Building)
			{
				if (HarvesterOnMinerals + HarvesterOnGas + HarvesterOnOther == 0) return false;
			}
			return true;
		}

		public bool IsOver
		{
			get
			{
				if (Count < Goal.Count) return false;
				foreach (KeyValuePair<IEntity, int> keyVal in Goal)
				{
					int count = CountEntity(keyVal.Key) + CountProduction(keyVal.Key);
					if (count < keyVal.Value) return false;
				}
				return true;
			}
		}

		public bool Equals(EntityState other)
		{
			if (Minerals != other.Minerals) return false;
			if (Gas != other.Gas) return false;
			if (HarvesterOnGas != other.HarvesterOnGas) return false;
			if (HarvesterOnMinerals != other.HarvesterOnMinerals) return false;
			if (Count != other.Count) return false;
			if (Production.Count != other.Production.Count) return false;
			foreach (KeyValuePair<IEntity, int> keyVal in this)
			{
				int count = 0;
				other.TryGetValue(keyVal.Key, out count);
				if (count != keyVal.Value) return false;
			}
			foreach (KeyValuePair<IEntity, int> keyVal in Production)
			{
				int count = 0;
				other.Production.TryGetValue(keyVal.Key, out count);
				if (count != keyVal.Value) return false;
			}
			return true;
		}

		public List<BuildInformation> GetBuildableEntities(List<IEntity> entities)
		{
			List<BuildInformation> buildable = new List<BuildInformation>();
			IEntity producerToUse = null;
			IDictionary<IEntity, int> inputToUse = null;
			foreach (IEntity entity in entities)
				if (CanBuild(entity, out producerToUse, out inputToUse))
				{
					BuildInformation information = new BuildInformation();
					information.Entity = entity;
					information.Information.Time = Time;
					information.Information.Input = inputToUse;
					information.Information.Producer = producerToUse;
					buildable.Add(information);
				}
			return buildable;
		}

		public EntityState Build(IEntity entity, IEntity producerToUse, IDictionary<IEntity, int> inputToUse)
		{
			EntityState clone = new EntityState(this);
			IUnit unit = entity as IUnit;
			if (unit != null)
				clone.Food += unit.Food;
			clone.Gas -= entity.Gas;
			clone.Minerals -= entity.Minerals;
			clone.Supply += entity.Supply;
			clone.Time = Time;
			clone.MineralsSpent += entity.Minerals;
			clone.GasSpent += entity.Gas;
			BuildInformation information = new BuildInformation();
			information.Entity = entity;
			information.Information.Input = inputToUse;
			information.Information.Producer = producerToUse;
			information.Information.Time = Time;
			clone.BuildTimes.Add(information);
			clone.ChangeProduction(entity, 1);
			if (inputToUse != null)
				foreach (KeyValuePair<IEntity, int> keyVal in inputToUse)
					clone.ChangeEntity(keyVal.Key, -keyVal.Value);
			if (producerToUse != null)
				clone.ChangeProducing(producerToUse, 1);
			if (entity is Building)
			{
				if (clone.HarvesterOnMinerals > 0) clone.HarvesterOnMinerals--;
				else if (clone.HarvesterOnGas > 0) clone.HarvesterOnGas--;
				else if (clone.HarvesterOnOther > 0) clone.HarvesterOnOther--;
				else throw new Exception();
				clone.HarvesterOnBuilding++;
			}
			clone.UpdateIncomeRate();
			if (Goal != null)
				clone.EstimatedTimeLeft = clone.EstimateTimeLeft(Goal);
			return clone;
		}

		public void UpdateIncomeRate()
		{
			m_GasIncomePerGameSecond = IncomeUtil.GetGasIncome(HarvesterOnGas, 2) / 60.0;
			m_MineralIncomePerGameSecond = IncomeUtil.GetMineralsIncome(HarvesterOnMinerals, 0, 8) / 60.0;
		}

		public double CostScore(Cost cost) { return CostScore(cost, 0.3); }
		public double CostScore(Cost cost, double rateIfNoIncome)
		{
			double score = cost.Time;
			if (GasIncomePerGameSecond > 0.0) score += (cost.Gas / GasIncomePerGameSecond);
			else score += (cost.Gas / rateIfNoIncome);
			if (MineralIncomePerGameSecond > 0.0) score += (cost.Minerals / MineralIncomePerGameSecond);
			else score += (cost.Minerals / rateIfNoIncome);
			return score;
		}

		public double EstimateTimeLeft(EntityState state)
		{
			Cost total = new Cost(0, 0, 0);
			List<IEntity> requirementsAlreadyNeeded = new List<IEntity>();
			foreach (KeyValuePair<IEntity, int> keyVal in state)
			{
				int producers = 1;
				if (keyVal.Key.Producers.Count > 0)
				{
					producers = 0;
					double firstBuildCost = double.MaxValue;
					Cost firstCost = null;
					List<IEntity> bestAdded = null;
					foreach (IEntity producer in keyVal.Key.Producers)
					{
						List<IEntity> added = new List<IEntity>();
						int temp = CountEntity(producer) + CountProduction(producer);
						producers += temp;
						if (producers == 0)
						{
							Cost tmp = producer.BuildCost(true, this, requirementsAlreadyNeeded, added);
							double scoreTmp = CostScore(tmp);
							if(firstBuildCost > scoreTmp)
							{
								firstBuildCost = scoreTmp;
								firstCost = tmp;
								bestAdded = added;
							}
						}
					}
					if (producers <= 0 && firstCost != null)
					{
						total.Add(firstCost);
						requirementsAlreadyNeeded.AddRange(bestAdded);
						producers = 1;
					}
				}
				int production = CountProduction(keyVal.Key);
				int count = CountEntity(keyVal.Key) + production;
				if (count == 0)
				{
					count = 1;
					List<IEntity> added = new List<IEntity>();
					total.Add(keyVal.Key.BuildCost(true, this, requirementsAlreadyNeeded, added));
					requirementsAlreadyNeeded.AddRange(added);
				}
				int diff = Math.Max(0, keyVal.Value - count);
				if(production > 0)
					total.Time += MaxBuildTime(keyVal.Key);
				if (diff > 0)
				{
					Cost cost = keyVal.Key.BuildCost(false, this, requirementsAlreadyNeeded, null);
					cost.Multiply(diff);
					if (diff > producers)
					{
						int modProdu = diff % producers;
						cost.Time = (diff / producers) * keyVal.Key.Time;
						if (modProdu != 0)
							cost.Time += keyVal.Key.Time;
					}
					else
						cost.Time = keyVal.Key.Time;
					total.Add(cost);
				}
			}
			total.Minerals = Math.Max(0.0, total.Minerals - Minerals);
			total.Gas = Math.Max(0.0, total.Gas - Gas);
			return Math.Max(CostScore(total), 0.0);
		}
		public double EstimatedTotalTime { get { return /*Time +*/  EstimatedTimeLeft; } }

		public double MineralIncomePerGameSecond { get { return m_MineralIncomePerGameSecond; } }
		public double GasIncomePerGameSecond { get { return m_GasIncomePerGameSecond; } }
		public double ResourcesPerGameSecond { get { return MineralIncomePerGameSecond + GasIncomePerGameSecond; } }

		public TimeSpan TimeSpan { get { return new TimeSpan(0, 0, Time); } }

		public List<EntityState> GeneratePossibleStates(List<IEntity> entities, int maxWaitTime)
		{
			IEntity producerToUse = null;
			IDictionary<IEntity, int> inputToUse = null;
			List<BuildInformation> buildableInformation = GetBuildableEntities(entities);
			List<IEntity> buildable = new List<IEntity>();
			List<EntityState> possible = new List<EntityState>();
			int useableGeyser = CountEntity(EntityXmlLoader.Instance.Get("Refinery"));
			if (HarvesterOnMinerals > 0 && useableGeyser * 3 > HarvesterOnGas)
			{
				EntityState mineralsMoveToGas = new EntityState(this);
				mineralsMoveToGas.HarvesterOnGas++;
				mineralsMoveToGas.HarvesterOnMinerals--;
				mineralsMoveToGas.UpdateIncomeRate();
				if (Goal != null)
					mineralsMoveToGas.EstimatedTimeLeft = mineralsMoveToGas.EstimateTimeLeft(Goal);
				possible.Add(mineralsMoveToGas);
			}
			if (HarvesterOnGas > 0)
			{
				EntityState gasMoveToMinerals = new EntityState(this);
				gasMoveToMinerals.HarvesterOnGas--;
				gasMoveToMinerals.HarvesterOnMinerals++;
				gasMoveToMinerals.UpdateIncomeRate();
				if (Goal != null)
					gasMoveToMinerals.EstimatedTimeLeft = gasMoveToMinerals.EstimateTimeLeft(Goal);
				possible.Add(gasMoveToMinerals);
			}
			foreach (BuildInformation information in buildableInformation)
			{
				possible.Add(Build(information.Entity, information.Information.Producer, information.Information.Input));
				buildable.Add(information.Entity);
			}
			if (maxWaitTime > 0)
			{
				EntityState clone = new EntityState(this);
				if (MineralIncomePerGameSecond == 0.0 && GasIncomePerGameSecond == 0.0) return possible;
				for (int c = 0; c < maxWaitTime; c++)
				{
					clone.IncrementTime();
					if (buildable.Count != entities.Count)
						foreach (IEntity entity in entities)
							if (!buildable.Contains(entity) && clone.CanBuild(entity, out producerToUse, out inputToUse))
							{
								buildable.Add(entity);
								possible.Add(clone.Build(entity, producerToUse, inputToUse));
								if (buildable.Count == entities.Count)
									break;
							}
				}
				//possible.Add(clone);
			}
			return possible;
		}

		public void IncrementTime()
		{
			Minerals += MineralIncomePerGameSecond;
			Gas += GasIncomePerGameSecond;
			Time++;
			for (int i = NextBuildTimeIndex; i < BuildTimes.Count; i++)
			{
				BuildInformation action = BuildTimes[i];
				if (action.Information.Time + action.Entity.Time == Time)
				{
					if (i == NextBuildTimeIndex)
					{
						int currentIndex = i + 1;
						while (currentIndex < BuildTimes.Count && BuildTimes[currentIndex].Information.Time + BuildTimes[currentIndex].Entity.Time < Time)
							currentIndex++;
						NextBuildTimeIndex = currentIndex;
					}
					bool updateRate = false;
					ChangeEntity(action.Entity, 1);
					ChangeProduction(action.Entity, -1);
					if (action.Information.Producer != null)
						ChangeProducing(action.Information.Producer, -1);
					if (action.Entity is Building)
					{
						HarvesterOnBuilding--;
						if (action.Entity.Name == "Refinery")
							HarvesterOnGas++;
						else
							HarvesterOnMinerals++;
						updateRate = true;
					}
					if (action.Entity.Name == "SCV")
					{
						HarvesterOnMinerals++;
						updateRate = true;
					}
					if (updateRate)
						UpdateIncomeRate();
				}
			}
		}

		public object Clone() { return new EntityState(this); }
	}
}
